package de.hmm.hwip.security.service;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

import android.os.Environment;
import android.util.Base64;
import android.util.Log;
import de.hmm.hwip.security.api.ISecurityServer;

/**
 * Class to handle de- and encryption with AES and RSA algorithm.
 * 
 * @author johannes mendel
 * @version 30.10.2012
 */
public class ServerSecurityImpl implements ISecurityServer, Runnable {

	/** Class identifier to debug **/
	final String TAG = "Securitey Server Impl";
	
	/** AES key used for de- and encryption*/
	private SecretKey key;
	
	/** This vector initializes the AES algorithm */
	private IvParameterSpec initVector;
	
	/** private key used for de- and encryption */
	private static PrivateKey privateKey = null;
	
	/** Public key used for de- and encryption */
	private static PublicKey publicKey = null;
	
	/** public key modulo and exponent for calculating the key */ 
	private static BigInteger publicKey_modulus = null;
	private static BigInteger publicKey_exponent = null;
	
	/** Path of private and public key **/
	private String path ="";
	
	/**
	 * Constructs a new security service implementation.
	 * 
	 */
	public ServerSecurityImpl(){
		try{
			//getting direction
			this.path = Environment.getExternalStorageDirectory().getAbsolutePath();
			this.path += "/hwip/security/";
			
	        // generate first time the AES key here - than all 24h
			KeyGenerator kgen = KeyGenerator.getInstance("AES");
	        kgen.init(128); 
	       
			this.key = kgen.generateKey();
		    this.initVector = new IvParameterSpec(this.key.getEncoded());
		    			
			//check if thread had been started before
			if(privateKey == null)
				privateKey = loadPrivateKey();
			
			// start actualization every 24h
			new Thread(this).start();
		    
		}catch(Exception e){
			new CryptoException(e.getLocalizedMessage(), e);
		}
	}
	
	/**
	 * Decrypting the given message with AES algorithm.
	 * 
	 * @param message - String to decrypt
	 * 
	 * @return decrypted string
	 */
	@Override
	public String decryptStringAES(String message) {
		try
		{
	        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
	        
	        cipher.init(Cipher.DECRYPT_MODE, this.key, initVector);
	        byte[] original = cipher.doFinal(Base64.decode(message, Base64.DEFAULT));
	        
	        return new String(original);
		}
		catch(BadPaddingException bpe){
			new CryptoException("BPE: "+ bpe.getLocalizedMessage(), bpe);
			return "bpe";
		}
		catch(Exception e)
		{
			new CryptoException(e.getLocalizedMessage(), e);
		}
		return null;
	}

	/**
	 * Decrypting the given message
	 * 
	 * @param message - string to decrypt
	 * 
	 * @return decrypted string
	 */
	@Override
	public String decryptStringRSA(String message) {
		try 
	    {
			//creating the cipher
			Cipher cipher = Cipher.getInstance("RSA/ECB/NoPadding");
			cipher.init(Cipher.DECRYPT_MODE, privateKey);
			
			byte[] cipherData = Base64.decode(message, Base64.DEFAULT); //decoding base64
			cipherData = cipher.doFinal(cipherData); //decrypt
			
			return new String(cipherData);
		} 
		catch (Exception e) 
		{
			new CryptoException(e.getLocalizedMessage(), e);
		}
		return null;
	}

	/**
	 * Encrypting the given message with AES algorithm.
	 * 
	 * @param message - String to encrypt
	 * 
	 * @return encrypted, base64 encoded string.
	 */
	@Override
	public String encryptStringAES(String message) {
		try 
		{
			//configuring the cipher.
			Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
			cipher.init(Cipher.ENCRYPT_MODE, this.key, initVector);

			return new String(Base64.encode(cipher.doFinal(message.getBytes()), Base64.NO_WRAP));
		}
		catch(BadPaddingException bpe){
			new CryptoException(bpe.getLocalizedMessage(), bpe);
			return "bpe";
		}
		catch (Exception e) 
		{
			new CryptoException(e.getLocalizedMessage(), e);
		} 
		return null;
	}

	/**
	 * Encrypting the given message
	 * 
	 * @param message - string to encrypt
	 * 
	 * @return encrypted, base64 encoded string.
	 */
	@Override
	public String encryptStringRSA(String message) {
		try 
	    {
			//creating the cipher
			Cipher cipher = Cipher.getInstance("RSA/ECB/NoPadding");
			cipher.init(Cipher.ENCRYPT_MODE, privateKey);

			byte[] cipherData = cipher.doFinal(message.getBytes()); //encrypt

			return Base64.encodeToString(cipherData, Base64.DEFAULT);
		} 
	    catch (Exception e) 
	    {
	    	new CryptoException(e.getLocalizedMessage(), e);
		}
		return null;
	}

	/**
	  * returns the current key for AES algorithm
	  * 
	  * @return the AES key
	  */
	@Override
	public String getAESKey() {
		byte[] raw = key.getEncoded(); 
		return Base64.encodeToString(raw, Base64.DEFAULT);
	}

	/**
	 * Gives the public Key (modulud and exponent) for RSA algorithm
	 * 
	 * @return RSA keys
	 */
	@Override
	public String[] getRSAKey() {
		if(publicKey == null)
		{
			publicKey = loadPublicKey();
			String[] key = {publicKey_modulus.toString(), publicKey_exponent.toString()};
			return key;
		}
		return null;
	}

	/** is called when thread is started, every 24 hours */
	@Override
	public void run() {
		//getting direction
		this.path = Environment.getExternalStorageDirectory().getAbsolutePath();
		this.path += "/hwip/security/";
		
		while(true)
		{
			// let the thread sleep for 24 hours
			try{
				Thread.sleep(1000*60*60*24);
				
			}catch(InterruptedException e)
			{
				Log.e(TAG, "Wait for security Thread exception: " + e.getMessage());
			}
			
			try{
		        // generate the AES key 
				KeyGenerator kgen = KeyGenerator.getInstance("AES");
		        kgen.init(128); 
		       
		        //set key and Vector
				this.key = kgen.generateKey();
			    this.initVector = new IvParameterSpec(this.key.getEncoded());
			    
			}catch(Exception e){
				new CryptoException(e.getLocalizedMessage(), e);
			}
		}
	}
	
	/**
	 * Loading the RSA private key from the file 'private.key'
	 * 
	 * @return The loaded public key.
	 * 
	 * @throws CryptoException Is thrown if loading, casting or creating fails.
	 */
	public PrivateKey loadPrivateKey() 
	{
		try
		{
			Log.i(TAG, "load private Key");
			//creating the streams
			InputStream in = new FileInputStream(new File(this.path + "private.key"));
			ObjectInputStream ois = new ObjectInputStream(in);
			
			//reading objects
			BigInteger modulus = (BigInteger) ois.readObject();
			BigInteger exponent = (BigInteger) ois.readObject();
			
			//creating private keySpec
			KeyFactory fact = KeyFactory.getInstance("RSA");
			RSAPrivateKeySpec privateKeySpec = new RSAPrivateKeySpec(modulus, exponent);

			return fact.generatePrivate(privateKeySpec); //generating the private key
		}
		 catch (Exception e) 
		{
			 new CryptoException(e.getLocalizedMessage(), e);
		}
		return null;
	}
	
	/**
	 * Loading the private key from the file: private.key
	 * 
	 * @return The loaded public key
	 * 
	 * @throws CryptoException - thrown if loading, casting or creating fails
	 */
	public PublicKey loadPublicKey()
	{
		try
		{
			//creating the streams
			InputStream in =  new FileInputStream(new File(this.path + "public.key"));
			ObjectInputStream ois = new ObjectInputStream(in);
			
			//reading objects
			BigInteger modulus = (BigInteger) ois.readObject();
			BigInteger exponent = (BigInteger) ois.readObject();
			
			publicKey_exponent = exponent;
			publicKey_modulus = modulus;
			
			//creating public keySpec
			KeyFactory fact = KeyFactory.getInstance("RSA");
			RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(modulus, exponent);
			
			return fact.generatePublic(publicKeySpec); //generating the private key
		}
		 catch (Exception e) 
		{
			 new CryptoException(e.getLocalizedMessage(), e);
		}
		return null;
	}
	
	/**
	 * Generating a new key pair
	 * ATTENTION: If private key is replaced the public key 
	 * in the client must be replaced too.
	 */
	public void generateKeyPair() throws CryptoException
	{       
	    try
	    {
	    	//creating the generator
	        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
	        kpg.initialize(2048); //maximum keysize
	        KeyPair kp = kpg.genKeyPair();

	        //creating RSA key specs
	        KeyFactory fact = KeyFactory.getInstance("RSA");
	        RSAPublicKeySpec pub = fact.getKeySpec(kp.getPublic(),RSAPublicKeySpec.class);
	        RSAPrivateKeySpec priv = fact.getKeySpec(kp.getPrivate(),RSAPrivateKeySpec.class);
	        
	        //saving the keys
	        saveToFile("public.key", pub.getModulus(), pub.getPublicExponent());
	        saveToFile("private.key", priv.getModulus(), priv.getPrivateExponent());
	    }
	    catch (Exception e) 
	    {
	    	new CryptoException(e.getLocalizedMessage(), e);
		}
	}
	
	/**
	 * Writing the values into the given file in security direction.
	 * The values are written in the order:
	 * 	1. Modulus
	 * 	2. Exponent
	 *  
	 * They have to be read in the same order
	 * 
	 * @param fileName - name of the file
	 * @param mod - modulus value of the key
	 * @param exp - exponent value of the key
	 * 
	 * @throws CryptoException - if encryption fails for any reason (nested exception)
	 */
	private void saveToFile(String fileName, BigInteger mod, BigInteger exp) throws CryptoException 
	{
		ObjectOutputStream oout = null;
		try 
		{
			//creating the stream
			oout = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(this.path + fileName)));
			
			//writing values
			oout.writeObject(mod);
		    oout.writeObject(exp);
		} 
		catch (Exception e) 
		{
			throw new CryptoException(e.getLocalizedMessage(), e);
		} 
		finally 
		{
			try 
			{
				oout.close();
			} 
			 catch (Exception e) 
			 {
			    	Log.e(TAG, "Output stream close exception.");
			 }
		}
	}
}
